/* Proxies.cs -- 
   Copyright (C) 2008  Pacific Biosciences, Inc.  */


using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;

namespace SexpSharp.RPC
{
	public class Proxies
	{
		internal struct CacheEntry
		{
			internal CacheEntry(Assembly assembly)
			{
				this.assembly = assembly;
				access = DateTime.Now;
			}

			internal DateTime access;
			internal Assembly assembly;
		} 

		private static object cacheLock = new Object();
		private static Dictionary<Type, CacheEntry> cache;
		private static MethodInfo invokeRemote =
			typeof(ProxyBase).GetMethod("InvokeRemote",
										BindingFlags.NonPublic | BindingFlags.Instance,
										null,
										CallingConventions.Standard,
										new Type[] {
											typeof(string), typeof(Type), typeof(object[])
										},
										null);

		/// <summary>
		/// Generate an RPC proxy for the specified interface `If',
		/// which will connect using the RPC client `client'.
		///</summary>
		public static If GenerateProxy<If>(RPCClient client)
		{
			Type t = typeof(If);
			if (!t.IsInterface)
				throw new ProxyGenException("can only generate proxies for interfaces");

			Assembly assy = null;
			lock (cacheLock)
			{
				CacheEntry entry;
				if (cache != null)
				{
					if (cache.TryGetValue(t, out entry))
					{
						entry.access = DateTime.Now;
						assy = entry.assembly;
						cache[t] = entry;
					}
				}
			}

			if (assy == null)
			{
				assy = GenerateAssembly(t);
				lock (cacheLock)
				{
					if (cache == null)
						cache = new Dictionary<Type, CacheEntry>();
					cache[t] = new CacheEntry(assy);
				}
			}

			Type gentype = assy.GetTypes()[0];
			ConstructorInfo ci = gentype.GetConstructor(new Type[] { typeof(RPCClient) });
			if (ci == null)
				throw new ProxyGenException("no constructor found in type " + gentype);
			return (If) ci.Invoke(new object[] { client });
		}

		protected static Assembly GenerateAssembly(Type target)
		{
			Guid guid = Guid.NewGuid();
			AssemblyName name = new AssemblyName();
			name.Name = ".srpc-" + guid;
			name.Version = new Version();
			string modname = name.Name + ".dll";
			string typename = name.Name + "_" + target.Name;
			AssemblyBuilder assembly =
				AppDomain.CurrentDomain.DefineDynamicAssembly(name,
															  AssemblyBuilderAccess.RunAndSave);
			ModuleBuilder module = assembly.DefineDynamicModule(name.Name,
																modname);
			TypeBuilder type = module.DefineType(typename,
												 TypeAttributes.Class
												 | TypeAttributes.Sealed
												 | TypeAttributes.Public,
												 typeof(ProxyBase),
												 new Type[] { target });
			GenerateConstructor(type);
			GenerateMethods(type, target);
			type.CreateType();
			// assembly.Save(modname);
			return assembly;
		}

		protected static void GenerateConstructor(TypeBuilder type)
		{
			Type[] argtypes = new Type[] { typeof(RPCClient) };
			ConstructorBuilder ctor = type.DefineConstructor(MethodAttributes.Public,
															 CallingConventions.HasThis,
															 argtypes);
			ConstructorInfo basector = typeof(ProxyBase).GetConstructor(BindingFlags.Instance
																		| BindingFlags.NonPublic,
																		null,
																		CallingConventions.HasThis,
																		argtypes, null);
			ILGenerator ilgen = ctor.GetILGenerator();
			ilgen.Emit(OpCodes.Ldarg_0);
			ilgen.Emit(OpCodes.Ldarg_1);
			ilgen.Emit(OpCodes.Call, basector);
			ilgen.Emit(OpCodes.Ret);
		}

		protected static void GenerateMethods(TypeBuilder type, Type target)
		{
			MethodInfo[] methods = AllInterfaceMethods(target);
			foreach (MethodInfo method in methods)
			{
				string methodName = method.Name;
				ParameterInfo[] param = method.GetParameters();
				Type[] ptypes = new Type[param.Length];
				for (int i = 0; i < param.Length; i++)
				{
					if (param[i].IsOut || param[i].ParameterType.IsByRef)
						throw new ProxyGenException("`out' and `ref' parameters not supported");
					ptypes[i] = param[i].ParameterType;
				}
				MethodBuilder mb = type.DefineMethod(methodName,
													 MethodAttributes.Public | MethodAttributes.Virtual,
													 method.CallingConvention,
													 method.ReturnType,
													 ptypes);
				GenerateMethod(mb, method);
			}
		}

		protected static void GenerateMethod(MethodBuilder method, MethodInfo target)
		{
			string methodName = target.Name;
			object[] attr = target.GetCustomAttributes(typeof(SRPCMethodAttribute), true);
			if (attr != null && attr.Length > 0)
				methodName = ((SRPCMethodAttribute) attr[0]).methodName;
			ILGenerator ilgen = method.GetILGenerator();
			LocalBuilder local0 = ilgen.DeclareLocal(typeof(object[]));
			local0.SetLocalSymInfo("args");
			LocalBuilder local1 = null;
			if (target.ReturnType != typeof(void))
			{
				local1 = ilgen.DeclareLocal(typeof(Type));
				local1.SetLocalSymInfo("returnType");
			}
			ParameterInfo[] param = target.GetParameters();

			EmitLength(ilgen, param.Length);
			ilgen.Emit(OpCodes.Newarr, typeof(object));
			ilgen.Emit(OpCodes.Stloc_0);
			if (local1 != null)
			{
				ilgen.Emit(OpCodes.Ldtoken, target.ReturnType);
				ilgen.Emit(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle"));
				ilgen.Emit(OpCodes.Stloc_1);
			}

			for (int i = 0; i < param.Length; i++)
			{
				ilgen.Emit(OpCodes.Ldloc_0);
				EmitLength(ilgen, i);
				EmitLoadArg(ilgen, i+1);
				if (param[i].ParameterType.IsValueType)
					ilgen.Emit(OpCodes.Box, param[i].ParameterType);
				ilgen.Emit(OpCodes.Stelem_Ref);
			}

			ilgen.Emit(OpCodes.Ldarg_0);
			ilgen.Emit(OpCodes.Ldstr, methodName);
			if (target.ReturnType == typeof(void))
				ilgen.Emit(OpCodes.Ldnull);
			else
				ilgen.Emit(OpCodes.Ldloc_1);
			ilgen.Emit(OpCodes.Ldloc_0);
			ilgen.Emit(OpCodes.Call, invokeRemote);
			if (target.ReturnType == typeof(void))
				ilgen.Emit(OpCodes.Pop);
			else
			{
				if (target.ReturnType.IsValueType)
				{
					ilgen.Emit(OpCodes.Unbox, target.ReturnType);
					ilgen.Emit(OpCodes.Ldobj, target.ReturnType);
				}
				else if (target.ReturnType != typeof(object))
					ilgen.Emit(OpCodes.Castclass, target.ReturnType);
			}
			ilgen.Emit(OpCodes.Ret);
		}

		protected static MethodInfo[] AllInterfaceMethods(Type type)
		{
			List<Type> types = new List<Type>();
			types.Add(type);
			foreach (Type iface in type.GetInterfaces())
				types.Add(type);
			Dictionary<string, MethodInfo> methodset = new Dictionary<string, MethodInfo>();
			foreach (Type t in types)
			{
				foreach (MethodInfo method in t.GetMethods())
				{
					methodset[method.ToString()] = method;
				}
			}

			MethodInfo[] methods = new MethodInfo[methodset.Count];
			int i = 0;
			foreach (MethodInfo method in methodset.Values)
				methods[i++] = method;
			return methods;
		}

		protected static void EmitLoadArg(ILGenerator ilgen, int arg)
		{
			switch (arg)
			{
			case 0:
				ilgen.Emit(OpCodes.Ldarg_0);
				break;
			case 1:
				ilgen.Emit(OpCodes.Ldarg_1);
				break;
			case 2:
				ilgen.Emit(OpCodes.Ldarg_2);
				break;
			case 3:
				ilgen.Emit(OpCodes.Ldarg_3);
				break;
			default:
				if (arg > -128 && arg < 127)
					ilgen.Emit(OpCodes.Ldarg_S, arg);
				else
					ilgen.Emit(OpCodes.Ldarg, arg);
				break;
			}
		}

		protected static void EmitLength(ILGenerator ilgen, int length)
		{
			switch (length)
			{
			case 0:
				ilgen.Emit(OpCodes.Ldc_I4_0);
				break;
			case 1:
				ilgen.Emit(OpCodes.Ldc_I4_1);
				break;
			case 2:
				ilgen.Emit(OpCodes.Ldc_I4_2);
				break;
			case 3:
				ilgen.Emit(OpCodes.Ldc_I4_3);
				break;
			case 4:
				ilgen.Emit(OpCodes.Ldc_I4_4);
				break;
			case 5:
				ilgen.Emit(OpCodes.Ldc_I4_5);
				break;
			case 6:
				ilgen.Emit(OpCodes.Ldc_I4_6);
				break;
			case 7:
				ilgen.Emit(OpCodes.Ldc_I4_7);
				break;
			case 8:
				ilgen.Emit(OpCodes.Ldc_I4_8);
				break;
			default:
				if (length >= -128 && length <= 127)
					ilgen.Emit(OpCodes.Ldc_I4_S, length);
				else
					ilgen.Emit(OpCodes.Ldc_I4, length);
				break;
			}
		}
	}
}